package xuegao.study.mall.gateway.authorization;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.json.JSONUtil;
import com.nimbusds.jose.JWSObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;
import xuegao.study.mall.common.constant.AuthConstant;
import xuegao.study.mall.common.domain.UserDto;
import xuegao.study.mall.gateway.component.SpringUtil;
import xuegao.study.mall.gateway.config.IgnoreUrlsConfig;
import xuegao.study.mall.gateway.feign.RoleService;

import java.net.URI;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 鉴权管理器,用于判断是否有资源的访问权限
 */
@Component
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {

    private Logger logger = LoggerFactory.getLogger(AuthorizationManager.class);

    @Autowired
    private IgnoreUrlsConfig ignoreUrlsConfig;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
        ServerHttpRequest request = authorizationContext.getExchange().getRequest();
        URI uri = request.getURI();
        AntPathMatcher pathMatcher = new AntPathMatcher();
        // 白名单路径直接放行
        List<String> ignoreUrls = ignoreUrlsConfig.getUrls();
        for (String path : ignoreUrls) {
            if (pathMatcher.match(path, uri.getPath())) {
                return Mono.just(new AuthorizationDecision(true));
            }
        }
        // 跨域的预检请求,直接放行
        if (request.getMethod()== HttpMethod.OPTIONS) {
            return Mono.just(new AuthorizationDecision(true));
        }
        try {
            // 未带token的直接拒绝
            String token = request.getHeaders().getFirst(AuthConstant.JWT_TOKEN_HEADER);
            if (StringUtils.isEmpty(token)) {
                return Mono.just(new AuthorizationDecision(false));
            }
            String realToken = token.replace(AuthConstant.JWT_TOKEN_PREFIX, "");
            JWSObject jwsObject = JWSObject.parse(realToken);
            String userStr = jwsObject.getPayload().toString();
            UserDto userDto = JSONUtil.toBean(userStr, UserDto.class);
            String clientId = userDto.getClientId();
            // 不同用户体系登录不允许互相访问
            if (AuthConstant.ADMIN_CLIENT_ID.equals(clientId) && !pathMatcher.match(AuthConstant.ADMIN_SERVICE_PREFIX, uri.getPath())) {
                return Mono.just(new AuthorizationDecision(false));
            }
            if (AuthConstant.PORTAL_CLIENT_ID.equals(clientId) && pathMatcher.match(AuthConstant.ADMIN_SERVICE_PREFIX, uri.getPath())) {
                return Mono.just(new AuthorizationDecision(false));
            }
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("AuthorizationManager auth error", e);
            return Mono.just(new AuthorizationDecision(false));
        }
        // 非管理端路径直接放行
        if (!pathMatcher.match(AuthConstant.ADMIN_SERVICE_PREFIX, uri.getPath())) {
            return Mono.just(new AuthorizationDecision(true));
        }
        // 管理端路径需要校验权限
        // 优先从缓存中拿到角色对应的资源
        List<String> cacheAuthorities = findAuthoritiesFromCache(pathMatcher, uri);
        if (CollUtil.isNotEmpty(cacheAuthorities)) {
            // 认证通过且角色匹配的用户可访问当前路径
            return mono.filter(Authentication::isAuthenticated)
                    .flatMapIterable(Authentication::getAuthorities)
                    .map(GrantedAuthority::getAuthority)
                    .any(cacheAuthorities::contains)
                    .map(AuthorizationDecision::new)
                    .defaultIfEmpty(new AuthorizationDecision(false));
        }
        // 从远程接口获取,此处需要异步调用
        RoleService roleService = SpringUtil.getBean(RoleService.class);
        return roleService.initResourceRolesMap().flatMap(commonResponse -> {
            Map<String, List<String>> resourceRolesMap = commonResponse.getData();
            Iterator<String> iterator = resourceRolesMap.keySet().iterator();
            List<String> authorities = new ArrayList<>();
            while (iterator.hasNext()) {
                String pattern = iterator.next();
                if (pathMatcher.match(pattern, uri.getPath())) {
                    authorities.addAll(resourceRolesMap.get(pattern));
                }
            }
            authorities = authorities.stream().map(item -> AuthConstant.AUTHORITY_PREFIX + item).collect(Collectors.toList());
            // 认证通过且角色匹配的用户可访问当前路径
            return mono.filter(Authentication::isAuthenticated)
                    .flatMapIterable(Authentication::getAuthorities)
                    .map(GrantedAuthority::getAuthority)
                    .any(authorities::contains)
                    .map(AuthorizationDecision::new)
                    .defaultIfEmpty(new AuthorizationDecision(false));
        });
    }

    /**
     * 缓存中获取用户角色当前资源权限
     */
    private List<String> findAuthoritiesFromCache(AntPathMatcher pathMatcher, URI uri) {
        String key = AuthConstant.RESOURCE_ROLES_MAP_KEY;
        Map<Object, Object> resourceRolesMap = redisTemplate.opsForHash().entries(key);
        Iterator<Object> iterator = resourceRolesMap.keySet().iterator();
        List<String> authorities = new ArrayList<>();
        while (iterator.hasNext()) {
            String pattern = (String)iterator.next();
            if (pathMatcher.match(pattern, uri.getPath())) {
                authorities.addAll(Convert.toList(String.class, resourceRolesMap.get(pattern)));
            }
        }
        authorities = authorities.stream().map(i -> AuthConstant.AUTHORITY_PREFIX + i).collect(Collectors.toList());
        return authorities;
    }
}
